HTML 77.2%
TypeScript 10.5%
Python 9.6%
JavaScript 2.5%
1import { ArrowLeftRight } from 'lucide-react';2import { ScrollX } from '@/components/models/scroll-x';3import type { Metadata } from 'next';4import Link from 'next/link';5import { notFound } from 'next/navigation';6import { ComparabilityBadge, TrustBadge } from '@/components/models/badges';7import { fmtScoreUnit } from '@/components/models/shared';8import { Chip, EntityBadge } from '@/components/ui/badges';9import { DataTable, Td, Th } from '@/components/ui/data-table';10import { EntityLink } from '@/components/ui/entity';11import { Container, Note, PageHeader } from '@/components/ui/section';12import { EmptyState, Unavailable } from '@/components/ui/unavailable';13import { ApiError, apiD1, safe } from '@/lib/api';14import { cn } from '@/lib/cn';15import { fmtDate, fmtInt, fmtTokens, fmtUsdPerM, fmtValue, num } from '@/lib/format';16import { routes, SITE_NAME, SITE_URL } from '@/lib/site';17import type { DiffDelta, ModelDiffPayload } from '@/lib/types';1819/*20 /models/[a]/diff/[b] — only the dimensions whose observed values differ, with deltas: context 200K → 1M (+400%), price −34%,21 benchmark ± points (comparable groups only), capability lists as added / removed. Shareable; linked from /compare.22*/2324type Params = { params: Promise<{ slug: string; b: string }> };25export const revalidate = 300;2627async function load(a: string, b: string): Promise<{ res: ModelDiffPayload | null; error: string | null }> {28 try {29 return { res: await apiD1.modelDiff(a, b), error: null };30 } catch (e) {31 if (e instanceof ApiError && e.notFound) notFound();32 if (e instanceof ApiError && (e.status === 400 || e.status === 422)) return { res: null, error: e.detail ?? 'These two entities cannot be diffed.' };33 return { res: null, error: null };34 }35}3637export async function generateMetadata({ params }: Params): Promise<Metadata> {38 const { slug, b } = await params;39 const res = await safe(apiD1.modelDiff(slug, b));40 const canonical = `/models/${encodeURIComponent(slug)}/diff/${encodeURIComponent(b)}`;41 if (!res) return { title: 'Model diff', robots: { index: false }, alternates: { canonical } };42 const title = `${res.a.name} vs ${res.b.name} — what differs`;43 const description = `${res.dimensions.length} differing dimensions between ${res.a.name} and ${res.b.name}: context, prices, benchmarks (comparable groups only), capabilities — each value with its source. ${SITE_NAME}.`;44 return { title, description, alternates: { canonical }, openGraph: { title: `${title} | ${SITE_NAME}`, description, url: `${SITE_URL}${canonical}`, type: 'article' } };45}4647function isNumDelta(d: DiffDelta): d is { absolute: number; percent: number | null } {48 return !!d && 'absolute' in d;49}50function isListDelta(d: DiffDelta): d is { added: unknown[]; removed: unknown[] } {51 return !!d && 'added' in d;52}5354function cell(kind: string, key: string, v: unknown, unit?: string): string {55 if (v === null || v === undefined || v === '' || (Array.isArray(v) && v.length === 0)) return 'Unavailable';56 if (kind === 'date') return fmtDate(String(v));57 if (kind === 'bool') return v ? 'Yes' : 'No';58 if (kind === 'list') return (Array.isArray(v) ? v : [v]).map(String).join(', ');59 if (kind === 'number') {60 if (/per_mtok/.test(key)) return fmtUsdPerM(v);61 if (key.startsWith('bench:')) return fmtScoreUnit(num(v), unit ?? null);62 if (/context_length|max_output_tokens/.test(key)) return fmtTokens(v);63 return fmtValue(v, key);64 }65 return typeof v === 'string' ? v : fmtValue(v, key);66}6768/** "+39 days" / "−2 months" between two ISO dates (month precision when either date is YYYY-MM). */69function dayDelta(a: string, b: string): string {70 const monthly = a.length <= 7 || b.length <= 7;71 const ta = new Date(a.length === 7 ? `${a}-01` : a.length === 4 ? `${a}-01-01` : a).getTime();72 const tb = new Date(b.length === 7 ? `${b}-01` : b.length === 4 ? `${b}-01-01` : b).getTime();73 if (!Number.isFinite(ta) || !Number.isFinite(tb)) return 'differs';74 const days = Math.round((tb - ta) / 86400000);75 const sign = days > 0 ? '+' : '−';76 const abs = Math.abs(days);77 if (monthly || abs >= 90) {78 const months = Math.round(abs / 30.44);79 return `${sign}${months} month${months === 1 ? '' : 's'}`;80 }81 return `${sign}${abs} day${abs === 1 ? '' : 's'}`;82}8384function fmtPct(p: number | null): string | null {85 if (p === null || !Number.isFinite(p)) return null;86 const sign = p > 0 ? '+' : p < 0 ? '−' : '';87 const abs = Math.abs(p);88 return `${sign}${abs >= 100 ? abs.toFixed(0) : abs.toFixed(abs < 10 ? 1 : 0)}%`;89}9091export default async function ModelDiffPage({ params }: Params) {92 const { slug, b } = await params;93 const { res, error } = await load(slug, b);94 const canonical = `/models/${encodeURIComponent(slug)}/diff/${encodeURIComponent(b)}`;95 if (!res)96 return (97 <Container>98 <PageHeader eyebrow="Model diff" title="What differs" />99 <div className="pb-16">{error ? <EmptyState title="These two entities cannot be diffed">{error}</EmptyState> : <Unavailable what="Model diff" reason="The API did not answer. Try again in a moment." />}</div>100 </Container>101 );102 const attr = res.dimensions.filter((d) => d.source !== 'results' && d.source !== 'prices');103 const prices = res.dimensions.filter((d) => d.source === 'prices');104 const bench = res.dimensions.filter((d) => d.source === 'results');105 const lists = res.dimensions.filter((d) => isListDelta(d.delta));106 const ld = { '@context': 'https://schema.org', '@type': 'Dataset', name: `${res.a.name} vs ${res.b.name} — differences`, url: `${SITE_URL}${canonical}`, description: res.note ?? undefined, about: [res.a, res.b].map((e) => ({ '@type': 'SoftwareApplication', name: e.name, url: `${SITE_URL}${routes.entity(e)}` })) };107108 const Row = ({ d }: { d: ModelDiffPayload['dimensions'][number] }) => {109 const comp = res.comparability?.[d.key];110 const nd = isNumDelta(d.delta) ? d.delta : null;111 const better = nd && d.kind === 'number' ? (d.source === 'prices' ? nd.absolute < 0 : (d.higher_is_better ?? true) ? nd.absolute > 0 : nd.absolute < 0) : null;112 return (113 <tr>114 <Td primary className="align-top">115 {d.key.startsWith('bench:') && d.benchmark ? (116 <Link href={routes.benchmark(d.benchmark) + (d.metric && d.config_key ? `?metric=${encodeURIComponent(d.metric)}&config_key=${encodeURIComponent(d.config_key)}` : '')} className="text-ink hover:text-accent hover:underline">117 {d.label.split(' · ')[0]}118 </Link>119 ) : (120 d.label121 )}122 <span className="block text-[11px] text-ink-3">123 {d.key.startsWith('bench:') ? d.label.split(' · ').slice(1).join(' · ') : d.unit}124 </span>125 {comp && (126 <span className="mt-0.5 flex flex-wrap items-center gap-1">127 <ComparabilityBadge level={comp.level} reasons={comp.reasons} />128 {comp.trust?.[res.a.id] && <TrustBadge level={comp.trust[res.a.id]?.level} label={comp.trust[res.a.id]?.label} />}129 </span>130 )}131 </Td>132 <Td label={res.a.name} className="tnum align-top">133 {cell(d.kind, d.key, d.a, d.unit)}134 </Td>135 <Td label={res.b.name} className="tnum align-top">136 {cell(d.kind, d.key, d.b, d.unit)}137 </Td>138 <Td label="Delta (b − a)" className="tnum align-top">139 {nd ? (140 <span className={cn('font-medium', better === true && 'text-positive', better === false && 'text-danger')}>141 {d.source === 'prices' ? `${nd.absolute > 0 ? '+' : '−'}${fmtUsdPerM(Math.abs(nd.absolute))}` : d.key.startsWith('bench:') ? `${nd.absolute > 0 ? '+' : '−'}${Math.abs(nd.absolute).toFixed(Math.abs(nd.absolute) < 10 ? 2 : 1)}${d.unit === '%' ? ' pt' : ''}` : `${nd.absolute > 0 ? '+' : '−'}${fmtValue(Math.abs(nd.absolute), d.key)}`}142 {fmtPct(nd.percent) && <span className="ml-1.5 text-xs font-normal text-ink-3">{fmtPct(nd.percent)}</span>}143 </span>144 ) : isListDelta(d.delta) ? (145 <span className="flex flex-wrap gap-1">146 {d.delta.added.map((x) => (147 <Chip key={`+${String(x)}`} className="text-positive bg-positive-soft">148 + {String(x)}149 </Chip>150 ))}151 {d.delta.removed.map((x) => (152 <Chip key={`-${String(x)}`} className="text-danger bg-danger-soft">153 − {String(x)}154 </Chip>155 ))}156 </span>157 ) : d.kind === 'date' && typeof d.a === 'string' && typeof d.b === 'string' ? (158 <span className="tnum text-ink-2">{dayDelta(d.a, d.b)}</span>159 ) : (160 <span className="text-ink-3">differs</span>161 )}162 </Td>163 </tr>164 );165 };166 const Table = ({ rows, caption }: { rows: ModelDiffPayload['dimensions']; caption: string }) => (167 <ScrollX><DataTable caption={caption} compact>168 <thead>169 <tr>170 <Th>Dimension</Th>171 <Th>{res.a.name}</Th>172 <Th>{res.b.name}</Th>173 <Th>Delta (b − a)</Th>174 </tr>175 </thead>176 <tbody>177 {rows.map((d) => (178 <Row key={d.key} d={d} />179 ))}180 </tbody>181 </DataTable></ScrollX>182 );183184 return (185 <Container wide>186 <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(ld) }} />187 <PageHeader188 eyebrow={189 <>190 <span>Model diff</span>191 <EntityBadge type="model" small />192 </>193 }194 title={195 <>196 {res.a.name} <span className="text-ink-3">vs</span> {res.b.name}197 </>198 }199 lede={`${fmtInt(res.dimensions.length)} dimensions differ. ${res.note ?? 'Only dimensions with differing observed values are listed; numeric delta = (b − a) / a.'}`}200 aside={201 <div className="flex flex-wrap gap-2 text-sm">202 <Link href={`/models/${encodeURIComponent(res.b.slug)}/diff/${encodeURIComponent(res.a.slug)}`} className="inline-flex h-9 items-center gap-1.5 border border-rule px-2.5 text-ink-2 hover:border-rule-strong hover:text-ink">203 <ArrowLeftRight className="size-3.5" aria-hidden /> Swap204 </Link>205 <Link href={routes.compare([res.a.slug, res.b.slug])} className="inline-flex h-9 items-center bg-ink px-3 font-medium text-canvas hover:opacity-90">206 Full comparison →207 </Link>208 </div>209 }210 >211 <p className="mt-4 flex flex-wrap gap-x-6 gap-y-1 text-sm text-ink-2">212 <span>213 a · <EntityLink e={res.a} className="font-medium" /> {res.a.organization && <span className="text-ink-3">{res.a.organization.name}</span>}214 </span>215 <span>216 b · <EntityLink e={res.b} className="font-medium" /> {res.b.organization && <span className="text-ink-3">{res.b.organization.name}</span>}217 </span>218 </p>219 </PageHeader>220 <div className="space-y-10 pb-16">221 {res.dimensions.length === 0 && <EmptyState title="No recorded difference">Every observed dimension has the same value for both models — or neither has a value.</EmptyState>}222 {attr.length > 0 && (223 <section>224 <p className="eyebrow mb-2">225 Specification & capabilities <span className="tnum text-ink-3">{attr.length}</span>226 </p>227 <Table rows={attr} caption="Differing specification dimensions" />228 </section>229 )}230 {prices.length > 0 && (231 <section>232 <p className="eyebrow mb-2">233 Pricing <span className="tnum text-ink-3">{prices.length}</span>234 </p>235 <Table rows={prices} caption="Differing price dimensions" />236 <Note className="mt-2">Best (cheapest) current offer across providers, USD per 1M tokens. A negative delta means b is cheaper.</Note>237 </section>238 )}239 {bench.length > 0 && (240 <section>241 <p className="eyebrow mb-2">242 Benchmarks <span className="tnum text-ink-3">{bench.length}</span>243 </p>244 <Table rows={bench} caption="Differing benchmark scores" />245 <Note className="mt-2">Only comparability groups where both models have a current result. Partially comparable = same task, conditions (effort, temperature, judge) differ. Not a verdict.</Note>246 </section>247 )}248 {lists.length > 0 && <Note>List dimensions ({lists.map((l) => l.label).join(', ')}) show elements added (+) and removed (−) going from a to b.</Note>}249 </div>250 </Container>251 );252}253